package br.edu.ufcg.greengrid.model;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.CollectionUtils;
import org.junit.Before;
import org.junit.Test;

import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.job.scheduler.FastestMachineJobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.JobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.SlowestMachineJobScheduler;
import br.edu.ufcg.greengrid.model.machine.Machine;
import br.edu.ufcg.greengrid.model.machine.state.SleepGoingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.SleepLeavingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;
import br.edu.ufcg.greengrid.model.machine.state.factory.SleepMachineStateFactory;

public class GridTest {

	private Grid g;

	@Before
	public void setUp() throws Exception {
		this.g = new Grid();
	}

	@Test
	public void testDoneGridWithNoMachines() {

		assertTrue(g.done());

	}

	@Test
	public void testDoneGridWithNoJobs() {

		g.setIdleMachine("machine1", 100);
		assertTrue(g.done());
	}

	@Test
	public void testDoneGridWithJobs() {

		long idletime = 100;
		long runtime = idletime / 2;
		Job j1 = new Job(runtime);
		Job j2 = new Job(2 * runtime);

		g.setIdleMachine("machine1", idletime);
		g.addJob(j1);
		g.addJob(j2);

		assertFalse(g.done());

		for (long i = 0; i < idletime; i++) {
			g.update();
		}

		assertFalse(g.done());
		assertTrue(j1.done());
		assertEquals(runtime, j1.getMakespan());
		assertEquals(2 * runtime, j2.getMakespan());
		
		g.setIdleMachine("machine1", idletime);
		for (long i = 0; i < idletime; i++) {
			g.update();
		}
		
		assertTrue(g.done());
		assertTrue(j2.done());
		assertEquals(4 * runtime, j2.getMakespan());
		
		List<Job> expectedJobs = new LinkedList<Job>();
		expectedJobs.add(j1);
		expectedJobs.add(j2);
		assertTrue(CollectionUtils.isEqualCollection(expectedJobs, g.getJobs()));
	}
	
	@Test
	public void testAddJob(){
		
		long runtime = 3;
		Job j1 = new Job(runtime);
		Job j2 = new Job(runtime);
		g.addJob(j1);
		g.addJob(j2);
		g.setIdleMachine("machine1", runtime);
		
		for (int i = 0; i < runtime; i++) {
			g.update();
		}
		
		assertTrue(j1.done());
		assertEquals(runtime, j1.getMakespan());
		
		assertFalse(j2.done());
		assertEquals(runtime, j2.getMakespan());

		List<Job> expectedJobs = new LinkedList<Job>();
		expectedJobs.add(j1);
		expectedJobs.add(j2);
		assertTrue(CollectionUtils.isEqualCollection(expectedJobs, g.getJobs()));
	}
	
	@Test
	public void testScheduleJobsFastestMachineJobScheduler() {

		testScheduleJobsWithSchedulers(new FastestMachineJobScheduler(), 2L, 4L, 4L);
	}
	
	@Test
	public void testScheduleJobsSlowestMachineJobScheduler() {
		
		testScheduleJobsWithSchedulers(new SlowestMachineJobScheduler(), 2L, 4L, 2L);
	}
	
	private void testScheduleJobsWithSchedulers(JobScheduler js, final long operationsPerSecondM1, final long operationsPerSecondM2, long operationsPerSecond){
		
		final String m1 = "m1";
		final String m2 = "m2";
		
		g.setScheduler(js);
		@SuppressWarnings("serial")
		Map<String, Long> machineIdToOperationsPerSecond = new HashMap<String, Long>(){{
			put(m1, operationsPerSecondM1);
			put(m2, operationsPerSecondM2);
		}};
		g.setMachineIdToOperationsPerSecond(machineIdToOperationsPerSecond);
		
		
		long runtime = 100;
		long expectedRuntime = runtime / operationsPerSecond;
		Job j = new Job(runtime);
		
		g.setIdleMachine(m1, runtime * 2);
		g.setIdleMachine(m2, runtime * 2);
		
		g.addJob(j);
		
		for (int i = 0; i < expectedRuntime; i++) {
			assertFalse("i = " + i, j.done());
			g.update();
		}
		
		assertTrue(j.done());
	}
	
	@Test
	public void testScheduleJobUntilItFails() {
		
		long maxJobFailures = 10;
		g.setMaxJobFailures(maxJobFailures);
		
		
		long runtime = 10;
		Job j = new Job(runtime);
		g.addJob(j);
		
		
		long idleTime = runtime - 1;
		String m1id = "m1";
		Machine m1 = null;
		
		for (int i = 0; i < maxJobFailures; i++) {
			g.setIdleMachine(m1id, idleTime);
			if(m1 == null){
				m1 = g.getMachines().get(0);//XXX Maybe we should provide a better way to retrieve specific machines. Or better ways to add them.
			}
			for (int k = 0; k < idleTime - 1; k++) {
				g.update();
				assertFalse(j.done());
				assertTrue("i = " + i + ", k = " + k, m1.hasJobAllocated());
			}
			
			g.update();
			assertFalse(j.done());
			assertFalse(m1.hasJobAllocated());
		}
		
		g.setIdleMachine(m1id, idleTime);
		g.update();
		assertFalse(m1.hasJobAllocated());
		assertFalse(j.done());
		
	}
	
	@Test
	public void testRescheduleJob() {
		
		long runtime = 2;
		Job j = new Job(runtime);
		g.addJob(j);
		long idletime = runtime / 2;
		g.setIdleMachine("machine1", idletime);
		
		for (int i = 0; i < runtime - 1; i++) {
			g.update();
			assertFalse(j.done());
		}
		
		g.update();
		assertFalse(j.done());
		assertEquals(runtime, j.getMakespan());
		
		g.setIdleMachine("machine1", runtime);
		for (int i = 0; i < runtime - 1; i++) {
			g.update();
			assertTrue("i = " + i, !j.done());
		}
		
		g.update();
		assertTrue(j.done());
		assertEquals(2 * runtime, j.getMakespan());
		
	}

	@Test
	public void testWakeUpMachines() {
		
		g.setMsf(new SleepMachineStateFactory());
		
		long runtime = 10;
		long waitTimeGoingGreen = SleepGoingGreenMachineState.WAIT_TIME;
		long waitTimeLeavingGreen = SleepLeavingGreenMachineState.WAIT_TIME;
		long idletime = runtime * runtime + waitTimeGoingGreen; //Make sure the machine can go green, go back and run the job.
		
		Job j = new Job(runtime);
		
		int nmachines = 10;
		for (int i = 1; i <= nmachines ; i++) {
			g.setIdleMachine("m" + i, idletime);
		}
		
		for (int i = 0; i < waitTimeGoingGreen - 1; i++) {
			g.update();
			assertEquals(nmachines, g.getMachines(MSEnum.GOING_GREEN).size());
		}
		g.update();
		
		g.addJob(j);
		
		for (int i = 0; i < waitTimeLeavingGreen; i++) {
			g.update();
			assertEquals("i = " + i, nmachines - 1, g.getMachines(MSEnum.GREEN).size());
		}

		for (int i = 0; i < runtime - 1; i++) {
			g.update();
			assertFalse(j.done());
		}
		
		g.update();
		assertTrue(j.done());
		
	}
	
	@Test
	public void testNumMachinesNotBeingUsedByUser() {
		
		assertEquals(0, g.getNumMachinesNotBeingUsedByUser());
		
		long idletime = 10;
		g.setIdleMachine("machine1", idletime);
		
		assertEquals(1, g.getNumMachinesNotBeingUsedByUser());
		
		for (int i = 0; i < idletime; i++) {
			g.update();
		}
		
		assertEquals(0, g.getNumMachinesNotBeingUsedByUser());
	}
	
	@Test
	public void testGridDemand() {
		
		assertEquals(0, g.getGridDemand());
		
		long runtime = 10;
		
		g.addJob(new Job(runtime));
		assertEquals(1, g.getGridDemand());
		
		g.addJob(new Job(2 * runtime));
		assertEquals(2, g.getGridDemand());
		
		g.setIdleMachine("machine1", 2 * runtime);
		g.setIdleMachine("machine2", 2 * runtime);
		
		for (int i = 0; i < runtime; i++) {
			g.update();
		}
		
		assertEquals(1, g.getGridDemand());
		
		for (int i = 0; i < runtime; i++) {
			g.update();
		}
		
		assertEquals(0, g.getGridDemand());
		
		
	}
	
	@Test
	public void testTimeSpentForMachineAtStateUser(){
		
		long idletime = 100;
		long usedByUserTime = 100;
		g.setIdleMachine("m1", idletime);

		for (int i = 0; i < idletime; i++) {
			g.update();
		}
		
		for (int i = 0; i < usedByUserTime; i++) {
			g.update();
		}
		
		Machine m = g.getMachines().get(0);
		
		assertEquals(idletime, m.getTotalTimeSpentAtState(MSEnum.IDLE));
		assertEquals(usedByUserTime, m.getTotalTimeSpentAtState(MSEnum.USER));
		
	}

}
